// Copyright (c) 2008 Matthew Johnson
// Licensed under the MIT License (see license.txt)
package actionml.animation {
	import actionml.Global
	
	import fl.transitions.easing.Regular;
	import flash.display.DisplayObject;	
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.getTimer;
	import flash.utils.setTimeout;
	
	public dynamic class Tween extends EventDispatcher {
		private static var _playingTweenCounts:Dictionary = new Dictionary;
		
		private var _name:String;
		public function get name():String { return _name; }		
		private var _target:Object;
		public function get target():Object { return _target; }
		private var _startState:String = "start";
		public function get startState():String { return _startState; }		
		private var _endState:String = "target";
		public function get endState():String { return _endState; }
		private var _ease:Function = Regular.easeInOut;
		public function get ease():Function { return _ease; }
		private var _duration:Number = 1000;
		public function get duration():Number { return _duration / 1000; }		
		internal function get renderDuration():Number { return _duration; }		
		private var _delay:Number = 0;
		private var _delayStarted:Number = -1;
		private var _properties:Object = {};
		private var _playWhenFinished:Array = [];
		private var _callWhenFinished:Array = [];

		private var _alwaysPlayFromStart:Boolean = false;
		public function get alwaysPlayFromStart():Boolean { return _alwaysPlayFromStart; }		
		private var _unlimitedRotation:Boolean = false;
		public function get unlimitedRotation():Boolean { return _unlimitedRotation; }
		private var _noAutoVisible:Boolean = false;
		public function get noAutoVisible():Boolean { return _noAutoVisible; }
		
		private var _start:Number;
		public function get startTime():Number { return _start; }
		private var _end:Number;
		public function get endTime():Number { return _end; }
		private var _reverse:Boolean = false;
		public function get shouldPlayBackwards():Boolean { return _reverse; }
		private var _playFromStart:Boolean = false;
		public function get shouldPlayFromStart():Boolean { return _playFromStart || _alwaysPlayFromStart; }
		private var _interrupted:int = 0;
		private var _completed:int = 0;
		private var _stopped:int = 0;
		private var _playing:Boolean = false;
		private var _rendererCount:int = 0;
		private var _renderers:Dictionary = new Dictionary();
		
		private static var _excludedProperties:Array = ["target", "name", "visible", "endState", "startState", "duration", "ease", "delay", "unlimitedRotation", "noAutoVisible", "alwaysPlayFromStart", "reverse", "playWhenFinished", "callWhenFinished"];
		private static var _rendererClasses:Array = [ColorTransformTweenRenderer, ColorPropertyTweenRenderer, RotationTweenRenderer, AlphaTweenRenderer, PropertyTweenRenderer];
		private static var _proxyClasses:Array = [FilterProxyRenderer];
		
		public function Tween(t:Object) {
			var tr:Object = Tween["getTracer"]({method:"Tween", context: "arg"});
			tr.info("The args provided to the tween are:", t);
			if (!t["target"]) {
				tr.warn("A tween needs a target in order to be played and has been created without one.  The target must be set before the tween can be played.");
			}
			_name = t["name"];
			_target = t["target"];
			_startState = t["startState"] || _startState;
			_endState = t["endState"] || _endState;
			_delay = t["delay"] * 1000 || _delay;
			_duration = t["duration"] * 1000 || _duration;
			if ("alwaysPlayFromStart" in t) {
				_alwaysPlayFromStart = t["alwaysPlayFromStart"];
			}
			if (t["ease"] is Function) {
				_ease = t["ease"];
			} else if (t["ease"] is String){
				_ease = Class["forName"](t["ease"].withoutLastPart())[t["ease"].lastPart()];
			}
			if ("playWhenFinished" in t) {
				if (t["playWhenFinished"] is Array) {
					for each (var o:Object in t["playWhenFinished"]) {
						this.playWhenFinished(o);
					}
				} else {
					this.playWhenFinished(t["playWhenFinished"]);
				}
			}
			if ("callWhenFinished" in t) {
				this.callWhenFinished(t["callWhenFinished"]);
			}
			for each (var p in t.getPropertyNames()) {
				if (!_excludedProperties.contains(p)) {
					_properties[p] = t[p];
				}
			}
			initializeProperties();
			Tween["getTracer"]({method:"Tween", context: "result"}).info("A new tween has been created:", this);
		}
		public function playWhenFinished(t:Object) {
			var result:Tween;
			if (t is Tween) {
				result = t as Tween;
			} else {
				if (t is String) {
					t = _target["tweens"][t];
				}
				if (!t.target) {
					t.target = _target;
				}
				if (!(t is Tween)) {
					result = new Tween(t);
				}
			}
			_playWhenFinished.push(result);
			return result;
		}
		public function callWhenFinished(f:Function) {
			_callWhenFinished.push(f);
			return this;
		}
		private function initializeProperties() {
			for each (var property:String in _properties.getPropertyNames()) {
				if (!property["startsWith"](_startState) && !property["startsWith"](_endState)) {
					initializeProperty(this, property);
				}
			}
			_target.walkTree(function(o:Object) {
				var path:String = "";
				var parent:Object = o;
				while (parent != _target) {
					path = parent["name"] + "." + path;
					parent = parent["parent"];
				}
				for each (var property:String in o.getPropertyNames()) {
					var fullPropertyPath:String = path + property;
					if (!(fullPropertyPath in _renderers)) {
						initializeProperty(o, fullPropertyPath);
					}
				}
				if (o is DisplayObject) {
					for each (var p:String in ["X", "Y", "Alpha", "Rotation"]) {
						if (_startState + p in o || _endState + p in o) {
							initializeProperty(o, path + p["toCamelCase"]())
						}
					}
				}
			});
		}
		private function initializeProperty(source:Object, fullPropertyPath:String) {
			var propertyName:String = fullPropertyPath["lastPart"]();		
			var propertyPath:String = fullPropertyPath["withoutLastPart"]();		
			var render:Boolean=false;
			if (!_excludedProperties.contains(propertyName)) {
				if (propertyName["startsWith"](_startState)) { // do we want to render if start state is defined and no end state is defined???
					propertyName = propertyName["withoutPrefix"](_startState);
					render = true;
				} else if (propertyName["startsWith"](_endState)) {
					propertyName = propertyName["withoutPrefix"](_endState);
					render = true;
				} else if (source == this) { 
					// properties that don't include start or end prefix for a tween are treated as an implicit end value
					render = true;
				}
				if (propertyPath == "symbolOpen" || propertyPath == "xmlOpen") {
					propertyPath = "open" // TODO: remove need for hack by fixing xml parsing
				}
				if (render && !(propertyPath + "." + propertyName in _renderers)) {
					var renderer:IRendererInternal = makeRenderer(propertyName, propertyPath);
					renderer.addEventListener(RenderEvent.INTERRUPT_RENDERING, this.rendererInterrupted, false, 0, true);
					renderer.addEventListener(RenderEvent.COMPLETE_RENDERING, this.rendererComplete, false, 0, true);
					renderer.addEventListener(RenderEvent.STOP_RENDERING, this.rendererStopped, false, 0, true);
					_rendererCount++;						
					_renderers[propertyPath + "." + propertyName] = renderer;
				}
			}
		}
		private function makeRenderer(propertyName:String, propertyPath:String):IRendererInternal {
			var t:Object = Tween["getTracer"]({method:"makeRenderer", context: "arg"});
			t.info("Tween.makeRenderer(propertyName:", propertyName, ", propertyPath:", propertyPath, ")");
			var proxyName:String = propertyPath["lastPart"]();
			var proxyClass:Class = findProxyClass(proxyName);
			var targetPath:String = propertyPath;
			var rendererClass:Class;
			if (proxyClass) {
				targetPath = propertyPath["withoutLastPart"]();
				rendererClass = findRendererClass(proxyClass["getProxyTargetClass"](proxyName), propertyName);
			} else {
				proxyName = "";
				rendererClass = findRendererClass(_target.constructor, propertyName);
			}
			var target:Object = _target.getValueForKeypath(targetPath);
			var startValue:Object = lookupValue(target, proxyName, _startState, propertyName);
			var endValue:Object = lookupValue(target, proxyName, _endState, propertyName, true);
			if (proxyClass) {
				return new proxyClass(target, proxyName, new rendererClass(null, propertyName, startValue, endValue, this));
			} else {
				return new rendererClass(target, propertyName, startValue, endValue, this);
			}
		}
		private function findProxyClass(proxyName:String):Class {
			if (!proxyName) {
				return null;
			}
			for each (var c:Class in _proxyClasses) {
				if (c["isApplicable"](proxyName)) {
					return c; 
				}
			}
			return null;
		}	
		private function findRendererClass(targetClass:Class, property:String):Class {
			for each (var c:Class in _rendererClasses) {
				if ("isApplicable" in c && c["isApplicable"] is Function) {
					if (c["isApplicable"](targetClass, property)) {
						return c; 
					}
				} else {
					Tween["getTracer"]({method:"findRendererClass", context: "interface"}).error(c, "does not implement isApplicable.")
				}
			}
			return null;
		}
		private function lookupValue(target:Object, proxyName:String, prefix:String, propertyName:String, allowDirectValue:Boolean=false) {
			var t:Object = Tween["getTracer"]({method:"lookupValue", context: "arg"});
			t.info("Tween.lookupValue(target: ", target, ", proxyName:", proxyName, ", prefix:", prefix, ", propertyName:", propertyName, ", allowDirectValue:", allowDirectValue, ")");
			var fullPropertyName:String = prefix + propertyName["toCapitalCase"]();
			if (proxyName) {
				proxyName = proxyName + ".";
			}
			var current:Object = target;
			var pathToTarget:String = "";
			var pairs:Array = [{object:current, pathToTarget: pathToTarget}];
			while (_target != current) {
				pathToTarget = current["name"] + "." + pathToTarget;
				current = current["parent"];
				pairs.unshift({object: current, pathToTarget: pathToTarget});
			}
			if (pathToTarget + proxyName + fullPropertyName in _properties) {
				return getDirectOrIndirectValue(_properties, pathToTarget + proxyName + fullPropertyName);
			}
			if (allowDirectValue && pathToTarget + proxyName + propertyName in _properties) {
				return getDirectOrIndirectValue(_properties, pathToTarget + proxyName + propertyName);
			}
			for each (var p:Object in pairs) {
				if (p.pathToTarget + proxyName + fullPropertyName in p.object) {
					return getDirectOrIndirectValue(p.object, p.pathToTarget + proxyName + fullPropertyName);
				}
			}
			return NaN;
		}
		private function getDirectOrIndirectValue(o:Object, p:String) {
			if (o == _properties && o[p] != null && isNaN(Number(o[p])) && o[p].startsWith("{")) {
				return o[p].replace(/^{|}$/g, "");
			}
			var result:Object = o.getValueForKeypath(p);
			if (result is Number) {
				return result;
			} else if (o == _properties) {
				return _target.getValueForKeypath(p["withoutLastPart"]())["evaluateExpression"](result);
			} else {
				return o.getValueForKeypath(result);
			}/*
			if (isNaN(o[p])) {
				if (o[o[p]] || o != _properties) {
					return o[o[p]];
				} else {
					return _target.getValueForKeypath(p["withoutLastPart"]())[o[p]];
				}
			} else {
				return o[p];
			}*/
		}
		public function play() {
			var t:Object = Tween["getTracer"]({method:"findRendererClass", context: "action"});
			t.info("Tween.play was called on :\n\t", this);
			if (_playing) {
				t.warn("Tween.play was called on a tween that is already playing.\n\t", this);
				return;
			}
			_playing = true;
			_interrupted = 0;
			_completed = 0;
			_stopped = 0;
			_playingTweenCounts[_target] ||= 0;
			_playingTweenCounts[_target]++;
			if (_delay > 0) {
				_delayStarted = getTimer();
				setTimeout(this.startPlaying, _delay);
			} else {
				this.startPlaying();
			} 
		}
		public function playFromStart() {
			_playFromStart = true;
			play();
		}
		public function playBackwards() {
			_reverse = true;
			play();
		}
		public function playBackwardsFromEnd() {
			_reverse = true;
			_playFromStart = true;			
			play();
		}
		private function startPlaying() {
			var delayStarted:Number = _delayStarted;
			_delayStarted = -1;
			if ("delayedTweensCancelled" in _target && _target["delayedTweensCancelled"] && delayStarted > 0 && delayStarted < _target["delayedTweensCancelled"]) {
				// delayed tweens for the target were cancelled after the delay was started
				_playing = false;
				return;
			}
			this.sendEvent(new TweenEvent(TweenEvent.PLAY_TWEEN, this));
			_start = new Date().getTime();
			_end = _start + _duration;
			for each (var renderer:IRenderer in _renderers) {
				RenderManager.addRenderer(renderer);
			}
		}
		private function rendererInterrupted(e:RenderEvent) {
			_interrupted++;
		}
		private function rendererComplete(e:RenderEvent) {
			_completed++;
		}
		private function rendererStopped(e:RenderEvent) {
			_stopped++;
			if (_stopped == _rendererCount && _interrupted + _completed == _rendererCount) {
				_playing = false;
				_reverse = false;
				_playFromStart = false;
				this.sendEvent(new TweenEvent(TweenEvent.COMPLETE_TWEEN, this));
				_playingTweenCounts[_target]--;
				for each (var t:Tween in _playWhenFinished) {
					t.play();
				}
				for each (var f:Function in _callWhenFinished) {
					f();
				}
				// this must come after playWhenFinished and callWhenFinished in case additional tweens start playing
				if (_playingTweenCounts[_target] == 0) {
					delete _playingTweenCounts[_target];
					this.sendEvent(new TweenEvent(TweenEvent.COMPLETE_ALL_TWEENS, this));
				}
			}
		}
		private function sendEvent(e:TweenEvent) {
			this.dispatchEvent(e);
			if ("doEvent" in _target && _target["doEvent"] is Function) {
				_target["doEvent"](e);
				if (this.name && e.type != "completeAllTweens") {
					_target["doEvent"](new TweenEvent(e.type.replace(/Tween$/, "") + this.name["toCapitalCase"](), this));
				}
			}
		}
		public override function toString():String {
			return "[Tween name: " + _name + "\nalwaysPlayFromStart: " + this.alwaysPlayFromStart + "\nshouldPlayFromStart: " + this.shouldPlayFromStart + "\n" + _properties + "\n]"
		}
	}
}